<?php 
/*
   Copyright 2010, e-TAT

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
   
   This is part of the e-TAT Architecture Package and will be featured in the final e-TAT Application (Phase 1).
*/
class Parser {
	private $reader = null;
	private $inside_item = array(); // Keep track of current position in tag tree
	private $current_tag = null; // Last entered tag name
	private $current_attr = null; // Attributes array of last entered tag
	private $namespaces = array(
		'http://purl.org/rss/1.0/'                  => 'RSS 1.0',
		'http://purl.org/rss/1.0/modules/content/'  => 'RSS 2.0',
		'http://www.w3.org/2005/Atom'               => 'ATOM 1',
	); // Namespaces to detact feed version
	private $item_tags = array('ITEM','ENTRY'); // List of tag names which holds a feed item
	private $channel_tags = array('CHANNEL','FEED'); // List of tag names which holds all channel elements
	private $date_tags = array('UPDATED','PUBDATE','DC:DATE');
	private $has_sub_tags = array('IMAGE','AUTHOR'); // List of tag names which have sub tags
	private $channels = array();
	private $items = array();
	private $item_index = 0;
	private $url = null; // The parsed url
	private $version = null; // Detected feed version

	function __construct() {
		$this->reader = xml_parser_create();
		xml_set_object($this->reader, $this);
		xml_set_element_handler($this->reader, "start", "end"); //Simple handlers for starting and ending the XML parser.
		xml_set_character_data_handler($this->reader, "characterData");
	}

	private function getUrlContent() {
		if(empty($this->url)) {
			die(false);
		}
		if($content = @file_get_contents($this->url)) { // Read the file if the URL points to the XML directly.
			return $content;
		} else { // If the URL points to a wrapper.
			$ch = curl_init();
			curl_setopt($ch, CURLOPT_URL, $this->url);
			curl_setopt($ch, CURLOPT_HEADER, false);
			curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
			$content = curl_exec($ch);
			$error = curl_error($ch);
			curl_close($ch);
			if(empty($error)) {
				return $content;
			} else {
				die(false);
			}
		}
	}

	private function findVersion($tag_name, $attrs) {
		$namespace = array_values($attrs);
		foreach($this->namespaces as $value =>$version) {
			if(in_array($value, $namespace)) {
				$this->version = $version;
				return;
			}
		}
	}

	private function getParentTag() {
		return $this->inside_item[count($this->inside_item) - 2];
	}

	private function start($parser, $tag_name, $attrs) {
		if(!$this->version) {
			$this->findVersion($tag_name, $attrs);
		}
		array_push($this->inside_item, $tag_name);
		$this->current_tag  = $tag_name;
		$this->current_attr = $attrs;
	}

	private function end($parser, $tag_name) {
		if (in_array($tag_name, $this->item_tags)) {
			$this->item_index++;
		}
		array_pop($this->inside_item);
		$this->current_tag = $this->inside_item[count($this->inside_item)-1];
	}

	private function inChannel() {
		if($this->version == 'RSS 1.0') {
			if(in_array('CHANNEL', $this->inside_item) && $this->current_tag != 'CHANNEL')
			return TRUE;
		} else if($this->version == 'RSS 2.0') {
			if(in_array('CHANNEL', $this->inside_item) && !in_array('ITEM', $this->inside_item) && $this->current_tag != 'CHANNEL')
			return TRUE;
		} else if($this->version == 'ATOM 1') { // Our case.
			if(in_array('FEED', $this->inside_item) && !in_array('ENTRY', $this->inside_item) && $this->current_tag != 'FEED')
			return TRUE;
		}
		return FALSE;
	}

	private function inItem() {
		if($this->version == 'RSS 1.0' || $this->version == 'RSS 2.0') {
			if(in_array('ITEM', $this->inside_item) && $this->current_tag != 'ITEM')
			return TRUE;
		} else if($this->version == 'ATOM 1') {
			if(in_array('ENTRY', $this->inside_item) && $this->current_tag != 'ENTRY')
			return TRUE;
		}
		return FALSE;
	}

	private function unhtmlentities($string) {
		// Get HTML entities table
		$trans_tbl = get_html_translation_table (HTML_ENTITIES, ENT_QUOTES);
		// Flip keys<==>values
		$trans_tbl = array_flip ($trans_tbl);
		// Add support for &apos; entity (missing in HTML_ENTITIES)
		$trans_tbl += array('&apos;' => "'");
		// Replace entities by values
		return strtr ($string, $trans_tbl);
	}

	public function getTotalItems() {
		return count($this->items);
	}

	public function getItems() {
		return $this->items;
	}

	public function getItem($index) {
		if($index < $this->getTotalItems()) {
			return $this->items[$index];
		} else {
			die(false);
		}
	}

	public function getChannels() {
		return $this->channels;
	}

	public function getChannel($tag_name) {
		if(array_key_exists(strtoupper($tag_name), $this->channels)) {
			return $this->channels[strtoupper($tag_name)];
		} else {
			die(false);
		}
	}

	public function getFeedVersion() {
		return $this->version;
	}

	public function getParsedUrl() {
		if(empty($this->url)) {
			return FALSE;
		} else {
			return $this->url;
		}
	}

	public function parse($url) {
		$this->url  = $url;
		$content = $this->getUrlContent();
		if($content) {
			$segments = str_split($content, 4096); //Limit.
			foreach($segments as $index=>$data) {
				$lastPiece = ((count($segments)-1) == $index)? true : false;
				xml_parse($this->reader, $data, $lastPiece)
				or die(false);
			}
			xml_parser_free($this->reader);
		} else {
			die(false);
		}
		if(empty($this->version)) {
			die(false);
		}
	}

	private function characterData($parser, $data) {
		//Converting all date formats to timestamp
		if(in_array($this->current_tag, $this->date_tags)) {
			$data = strtotime($data);
		}
		if($this->inChannel()) {
			// If has subtag, make current element an array and assign subtags as it's element
			if(in_array($this->getParentTag(), $this->has_sub_tags)) {
				if(! is_array($this->channels[$this->getParentTag()])) {
					$this->channels[$this->getParentTag()] = array();
				}
				$this->channels[$this->getParentTag()][$this->current_tag] .= strip_tags($this->unhtmlentities((trim($data))));
				return;
			} else {
				if(! in_array($this->current_tag, $this->has_sub_tags)) {
					$this->channels[$this->current_tag] .= strip_tags($this->unhtmlentities((trim($data))));
				}
			}
			if(!empty($this->current_attr)) {
				$this->channels[$this->current_tag . '_ATTRS'] = $this->current_attr;

				//If the tag has no value
				if(strlen($this->channels[$this->current_tag]) < 2) {
					//If there is only one attribute, assign the attribute value as channel value
					if(count($this->current_attr) == 1) {
						foreach($this->current_attr as $attr_val) {
							$this->channels[$this->current_tag] = $attr_val;
						}
					}
					//If there are multiple attributes, assign the attributs array as channel value
					else {
						$this->channels[$this->current_tag] = $this->current_attr;
					}
				}
			}
		} else if($this->inItem()) {
			// If has subtag, make current element an array and assign subtags as it's elements
			if(in_array($this->getParentTag(), $this->has_sub_tags)) {
				if(! is_array($this->items[$this->item_index][$this->getParentTag()])) {
					$this->items[$this->item_index][$this->getParentTag()] = array();
				}

				$this->items[$this->item_index][$this->getParentTag()][$this->current_tag] .= strip_tags($this->unhtmlentities((trim($data))));
				return;
			} else {
				if(! in_array($this->current_tag, $this->has_sub_tags)) {
					$this->items[$this->item_index][$this->current_tag] .= strip_tags($this->unhtmlentities((trim($data))));
				}
			}

			if(!empty($this->current_attr)) {
				$this->items[$this->item_index][$this->current_tag . '_ATTRS'] = $this->current_attr;

				//If the tag has no value
				if(strlen($this->items[$this->item_index][$this->current_tag]) < 2) {
					//If there is only one attribute, assign the attribute value as feed element's value
					if(count($this->current_attr) == 1) {
						foreach($this->current_attr as $attr_val) {
							$this->items[$this->item_index][$this->current_tag] = $attr_val;
						}
					} else { //If there are multiple attributes, assign the attribute array as feed element's value
						$this->items[$this->item_index][$this->current_tag] = $this->current_attr;
					}
				}
			}
		}
	}
}